Skip to content

S05-01 多线程-多线程

[TOC]

概述

进程与线程

  • 进程(Process):操作系统分配资源(如内存)的最小单位。一个独立运行的程序就是一个进程(比如你打开的一个浏览器)。
  • 线程(Thread):CPU 调度的最小单位。它是进程中的一个执行流程。一个进程可以包含多个线程,这些线程共享该进程的内存资源(如堆内存和方法区),但每个线程有自己独立的程序计数器(PC)和虚拟机栈

一句话总结: 进程就像是一辆火车,而线程就是火车上的一节节车厢。多线程就是让这列火车有多个车厢同时运作。

图示:JVM 中的线程

  • 不同的进程之间是不共享内存的。

  • 每个线程独立的拥有自己的:虚拟机栈、本地方法栈、程序计数器。一个进程中的多个线程,共享进程的:方法区、堆。

  • 进程之间的数据交换和通信的成本很高。

image-20220514175737426

并发与并行

这两个概念经常被混淆,但它们在计算机科学中有着明确的区别:

  • 并发 (Concurrency):指的是系统拥有处理多个任务的能力,但不一定是同时执行。在单核 CPU 中,操作系统通过快速切换时间片,让多个线程交替执行,使得宏观上看起来像是在“同时”运行。
  • 并行 (Parallelism):指的是系统拥有同时执行多个任务的能力。这通常需要多核 CPU 的支持,真正的在同一物理时刻,不同的核心在执行不同的线程。

比喻:

  • 并发是一个咖啡机,两个人排队交替接咖啡;
  • 并行是两台咖啡机,两个人同时各自接咖啡。

image-20260401164335096

示例:查看 CPU 核数

java
public static void main(String[] args) {
  Runtime runtime = Runtime.getRuntime();
  int cores = runtime.availableProcessors();
  System.out.println("CPU 核心数 = " + cores); // 16
}

线程调度

在 Java 多线程编程中,当有多个线程处于可运行(RUNNABLE)状态时,谁先执行、谁后执行、每个线程执行多久,这就涉及到了线程调度(Thread Scheduling)

理解线程调度,有助于我们明白为什么多线程程序的执行结果往往是“不可预测”的,以及我们能在多大程度上干预这种调度。

在计算机科学中,主要有两种线程调度模型:

协同式调度

方式一:协同式调度 (Cooperative Scheduling):

  • 机制: 线程的执行时间由线程本身控制。一个线程执行完自己的工作后,主动通知系统切换到另外一个线程。
  • 优点: 实现简单,没有线程同步的问题(因为什么时候切换是确定的)。
  • 缺点: 极其危险。如果一个线程编写有问题,一直不让出 CPU(比如死循环),会导致整个系统崩溃。

抢占式调度

方式二:抢占式调度 (Preemptive Scheduling) —— Java 的选择:

  • 机制: 线程的执行时间由操作系统(调度器) 来分配。操作系统会给每个线程分配一个“时间片”(Time Slice,通常是几十毫秒)。时间片用完,或者发生阻塞时,操作系统会强制剥夺该线程的 CPU 执行权,并把 CPU 交给其他处于就绪状态的线程。
  • 优点: 一个线程的阻塞或死循环不会导致整个系统崩溃,多任务并发性好。
  • 缺点: 线程切换频繁会有上下文切换(Context Switch)开销;会导致线程安全问题,需要开发者手动进行同步(加锁等)。

核心结论: Java 的线程调度是抢占式的。这意味着 Java 程序无法绝对控制哪个线程在什么时刻执行,只能对调度器提出“建议”。

抢占式调度

线程优先级

线程优先级 (Thread Priority):

既然调度由 OS 决定,Java 提供了 setPriority(int newPriority) 方法,试图给调度器一些“建议”。

Java 线程优先级范围是 1 到 10:

  • Thread.MIN_PRIORITY (1)
  • Thread.NORM_PRIORITY (5) —— 默认优先级
  • Thread.MAX_PRIORITY (10)

优先级规则与致命陷阱:

  1. 高优先级不等于先执行: 优先级高的线程只是获取 CPU 时间片的概率更大,绝不意味着它一定会在低优先级线程之前执行完毕。

  2. OS 映射差异: Java 有 10 个优先级,但底层操作系统可能没有这么多。比如 Windows 有 7 个,Linux 的某些调度策略下优先级可能完全被忽略。多个 Java 优先级可能被映射到同一个 OS 优先级上。

  3. 饥饿 (Starvation): 如果你把某个线程优先级设得极低,在 CPU 繁忙时,它可能永远抢不到时间片,导致“饿死”。

实战建议: 在实际的业务开发中,绝对不要依赖线程优先级来控制程序的业务逻辑和执行顺序! 它极不可靠。通常我们都保持默认的优先级(5)即可。

单核和多核

单核CPU:在一个时间单元内,只能执行一个线程的任务。

例如,可以把CPU看成是医院的医生诊室,在一定时间内执行一行代码(给一个病人诊断治疗)。所以单核CPU就是,代码经过前面一系列的前导操作(类似于医院挂号),然后到cpu处执行时发现,就只有一个CPU,大家排队执行。(类似于10个挂号窗口挂号,结果跑到医生那只有一个医生,只能排队等)。

这时候想要提升系统性能,只有两个办法,要么提升CPU性能(让医生看病快点),要么多加几个CPU(多整几个医生)。后者即为提供多核CPU。如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)。

问题:多核的效率是单核的倍数吗?

譬如4核A53的cpu,性能是单核A53的4倍吗?理论上是,但是实际不可能,至少有两方面的损耗。

  • 一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一台机器,性能瓶颈就从医生转到B超检查了。
  • 另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作一样,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到2倍。

线程创建方式

严谨地说,Java 中创建线程本质上只有一种方式:构造一个 java.lang.Thread 类的实例,并调用其 start() 方法。所谓的“多种方式”,其实是指封装线程执行任务(Task)的方式不同

以下是 Java 中常见的四种创建并启动线程的方式详解:

方式1:继承 Thread

这是最直观、最古老的方式。你只需要创建一个类继承 Thread,并重写它的 run() 方法

代码示例:

java
// 1. 自定义类继承 Thread
class MyThread extends Thread {

  // 2. 重写 Thread 类的 run 方法
  @Override
  public void run() {
    // 线程要执行的业务逻辑
    System.out.println(Thread.currentThread().getName() + " 正在执行...");
  }
}

public class ThreadDemo {
  public static void main(String[] args) {
    // 3. 创建线程对象
    MyThread t1 = new MyThread();
    MyThread t2 = new MyThread();

    // 4. 调用 start() 方法启动线程
    t1.start();
    t2.start();
  }
}
  • 优点: 编写简单,如果在 run() 方法内部需要获取当前线程,直接使用 this 即可,无须调用 Thread.currentThread()
  • 缺点: 极其不推荐在现代开发中使用。因为 Java 是单继承的,如果你的类已经继承了 Thread,就无法再继承其他业务类,大大限制了代码的扩展性。此外,任务(run 方法)和线程(Thread 对象)强耦合在一起

练习

  1. 创建一个分线程1,用于遍历 100 以内的偶数

    image-20260401170117850

  2. 创建两个分线程,一个线程用于遍历 100 以内的偶数,另一个线程用于遍历 100 以内的奇数

    方式一:使用标准方法

    image-20260401171741439

    方式二:使用匿名类的方式

    image-20260401172118608

方式2:实现 Runnable 接口

实现 Runnable 接口:

为了解决单继承的限制,Java 提供了 Runnable 接口。你将需要执行的任务写在 Runnable 实现类的 run() 方法中,然后将这个实现类作为一个“目标任务”丢给 Thread 对象去执行

写法一:标准写法:

java
// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {

  // 2. 实现 Runnable 接口的 run() 方法
  @Override
  public void run() {
    // 线程要执行的业务逻辑
    System.out.println(Thread.currentThread().getName() + " 正在执行 Runnable 任务...");
  }
}

public class RunnableDemo {
  public static void main(String[] args) {
    // 3. 创建当前任务对象
    MyRunnable task = new MyRunnable();

    // 4. 将任务对象传入 Thread 构造器,并创建 Thread 类的实例对象
    Thread t1 = new Thread(task, "线程-A");
    Thread t2 = new Thread(task, "线程-B");

    // 5. 调用 start() 方法启动线程
    t1.start();
    t2.start();
  }
}

写法二:匿名实现类写法

java
public class RunnableDemo {
  public static void main(String[] args) {象
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        // 线程要执行的业务逻辑
        System.out.println(Thread.currentThread().getName() + " 正在执行 Runnable 任务...");
      }
    }, "线程-A").start();
  }
}

写法三:现代写法(使用 Lambda 表达式,Java 8+):

java
Thread t3 = new Thread(() -> {
  System.out.println(Thread.currentThread().getName() + " 使用 Lambda 执行任务...");
}, "线程-C");
t3.start();

优缺点

  • 优点
  • 缺点: run() 方法没有返回值,也无法抛出受检异常(Checked Exception),只能在方法内部 try-catch

注意:Thread类实际上也是实现了Runnable接口的类(代理模式):

java
public class Thread extends Object implements Runnable

练习

  1. 程序阅读

    image-20260401180216584

方式3:实现 Callable 接口与 FutureTask

API:FutureTask

FutureTask API

构造方法

  • FutureTask()(Callable<V> callable)构造方法,将一个带有返回值的 Callable 任务包装成 FutureTask
  • FutureTask()(Runnable runnable, V result)构造方法,包装一个 Runnable 任务,并提前指定好一个返回值 result

实例方法

  • V get()()获取计算结果(死等派)。
    极其重要:如果任务还没执行完,调用这个方法的当前线程会被完全阻塞挂起,直到任务执行完毕返回结果,或者抛出异常。
  • V get()(long timeout, TimeUnit unit)获取计算结果(限时派,极度推荐)。
    如果到了指定时间任务还没出结果,会抛出 TimeoutException。在企业开发中,强烈禁止使用无参的 get() 以防止主线程被永远卡死。

实现 Callable 接口与 FutureTask

如果你希望线程执行完毕后能返回一个结果,或者能抛出异常供外部捕获,那么就需要使用 Callable 接口(Java 5 引入)。

因为 Thread 类的构造器只接受 Runnable,不接受 Callable,所以我们需要一个桥梁——FutureTaskFutureTask 实现了 Runnable 接口,同时它的构造器可以接收 Callable

代码示例:

java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

// 1. 实现 Callable 接口,泛型 <Integer> 代表返回值的类型
class MyCallable implements Callable<Integer> {
  @Override
  public Integer call() throws Exception {
    System.out.println(Thread.currentThread().getName() + " 正在计算...");
    Thread.sleep(2000); // 模拟耗时操作
    return 100 + 200;
  }
}

public class CallableDemo {
  public static void main(String[] args) throws Exception {
    // 2. 创建 Callable 任务
    MyCallable task = new MyCallable();

    // 3. 使用 FutureTask 包装 Callable
    FutureTask<Integer> futureTask = new FutureTask<>(task);

    // 4. 将 FutureTask 交给 Thread 执行
    Thread t1 = new Thread(futureTask, "计算线程");
    t1.start();

    System.out.println("主线程可以继续做其他事情...");

    // 5. 获取结果。注意:get() 方法会阻塞当前主线程,直到 call() 执行完毕并返回结果!
    Integer result = futureTask.get();
    System.out.println("计算结果是: " + result);
  }
}
  • 优点: 功能最强大,有返回值,能抛出异常。通过 FutureTask 还可以取消任务、判断任务是否完成。
  • 缺点: 代码相对繁琐。调用 get() 方法时如果没有设置超时时间,可能会导致阻塞。

方式4:使用线程池(推荐)@

使用线程池(Executor 框架):

在实际的企业级项目开发中,我们几乎从来不会手动去 new Thread()。因为频繁创建和销毁线程会消耗极大的系统资源,并且难以统一管理,容易导致内存溢出(OOM)。

业界标准做法是使用线程池。你只需要把 RunnableCallable 任务提交给线程池,线程池会自动分配工作线程来执行它们。

代码示例:

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadPoolDemo {
  public static void main(String[] args) throws Exception {
    // 1. 创建一个固定大小为 3 的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(3); 

    // 2. 提交 Runnable 任务 (使用 execute)
    threadPool.execute(() -> { 
      System.out.println(Thread.currentThread().getName() + " 执行无返回值的任务");
    });

    // 3. 提交 Callable 任务 (使用 submit)
    Future<String> future = threadPool.submit(() -> { 
      System.out.println(Thread.currentThread().getName() + " 执行有返回值的任务");
      return "Task Success"; 
    });
    System.out.println("Callable 返回值: " + future.get()); 

    // 4. 关闭线程池 (不再接受新任务,等待已有任务执行完毕)
    threadPool.shutdown(); 
  }
}

优点:

  1. 资源复用:避免频繁创建销毁线程。
  2. 响应速度快:任务来了直接拿池子里的空闲线程执行。
  3. 便于管理:可以控制最大并发数,提供定时执行、周期执行等功能。

底层原理

start() 源码

在 Java 中,启动一个线程我们总是调用 thread.start(),而不是直接调用 run()。你可能听过无数次“start() 才会真正创建新线程”,但它在底层究竟玩了什么魔法?

为了彻底看清它的真面目,我们需要跨越 Java 源码层 -> JVM 桥接层(C++) -> 操作系统内核层(OS)。这里我们主要看 Java 源码层,其他的略过(过于硬核)。

当我们调用 thread.start() 时,Java 层面做的事情相对简单,主要负责状态拦截线程组管理

java
public synchronized void start() {
  // 1. 状态校验:如果线程状态不是 NEW (threadStatus == 0),直接抛出异常
  // 这也是为什么同一个线程对象不能被 start() 两次的原因
  if (threadStatus != 0)
    throw new IllegalThreadStateException();

  // 2. 将当前线程加入到对应的线程组(ThreadGroup)中
  group.add(this);

  boolean started = false;
  try {
    // 3. 核心:调用本地方法 start0(),真正的大招在这里
    start0();  
    started = true;
  } finally {
    try {
      if (!started) {
        // 如果启动失败,通知线程组进行清理(回滚计数等)
        group.threadStartFailed(this);
      }
    } catch (Throwable ignore) {
      /* 忽略异常,让 start0 抛出的原始异常继续向上抛 */
    }
  }
}

// 这是一个 native 本地方法,由 C/C++ 实现,并由 JVM 调用
private native void start0();

start0() 是一个 native 本地方法,由 C/C++ 实现,并由 JVM 调用。它其实才是真正创建线程的方法。

调用 start0() 后,Java 向上打了个报告,JVM 向操作系统申请了一套真正的线程资源,并把你的 run() 方法作为新线程的启动任务交给了操作系统去执行

代理模式实现 Runable

在 Java 的多线程体系中,通过实现 Runnable 接口来创建线程是一种极其推荐的做法。而在这一设计的背后,隐藏着一个非常经典的面向对象设计模式——静态代理模式(Static Proxy Pattern)

可以说,Thread 类和 Runnable 接口之间的关系,就是教科书级的代理模式实现。

代理模式

在设计模式中,代理模式的定义是:为其他对象提供一种代理以控制对这个对象的访问

  • 代理类和真实处理业务的类实现同一个接口
  • 代理类不仅包含真实角色的功能,还可以进行功能增强(扩展)。

静态代理模式通常包含三个核心角色

  1. 抽象角色(Subject):定义了代理角色和真实角色共同的接口(Runable)。

  2. 真实角色(Real Subject):真正执行核心业务逻辑的角色(MyTask)。

  3. 代理角色(Proxy Subject):内部持有真实角色的引用,负责在执行真实角色逻辑的前后,进行额外的功能增强(Thread)。

线程中的代理模式角色

当我们写下如下代码时,这三个角色就已经各就各位了:

java
// 1. 真实角色:专注于业务逻辑
class MyTask implements Runnable {
  @Override
  public void run() {
    System.out.println("真正的业务逻辑正在由 " + Thread.currentThread().getName() + " 执行");
  }
}

public class Main {
  public static void main(String[] args) {
    // 1. 创建真实角色
    MyTask task = new MyTask();
    // 2. 创建代理角色,并将真实角色作为参数注入
    Thread thread = new Thread(task);
    // 3. 通过代理角色启动
    thread.start();
  }
}

它们是如何一一对应的呢?

  • 抽象角色 -> Runnable 接口

    它只定义了一个 public void run() 方法。无论是你的业务代码,还是底层的线程调度,大家都要遵守这个规范

  • 真实角色 -> 程序员自定义的实现类(如 MyTask

    它实现了 Runnable 接口,里面写的是你真正想让线程去执行的核心业务代码。它只关心“做什么”,而不关心“怎么分配 CPU 资源”。

  • 代理角色 -> Thread

    你可能没注意到,Thread 类本身也实现了 Runnable 接口(核心源码:public class Thread implements Runnable)。它负责“功能增强”,去处理复杂的底层线程创建、CPU 资源申请和销毁等工作。

代理模式运作机制

我们来看一下 Thread 类的内部源码,看看它是如何完美符合静态代理模式的:

代理类内部持有真实角色的引用

Thread 类中,有一个私有的成员变量 target,用来接收我们传进去的真实任务:

java
public class Thread implements Runnable {
  /* What will be run. */
  private Runnable target; // 真实角色的引用

  // 构造函数:注入真实角色
  public Thread(Runnable target) {
    this.init(null, target, "Thread-" + nextThreadNum(), 0);
  }

  // 内部初始化方法,最终将我们传进来的 task 赋值给 target
  private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    // ... 省略其他初始化代码
    this.target = target;
  }
}

代理类的 run() 方法进行内部转发

当底层操作系统分配好 CPU 资源,开始执行 Threadrun() 方法时,它其实是转发给了 target(也就是我们的真实角色):

java
@Override
public void run() {
  // 完美的代理转发逻辑!
  // 如果传了真实角色(target),就调用真实角色的 run() 方法
  if (target != null) {
    target.run();
  }
}

代理模式优点

如果直接继承 Thread 类也能创建线程,为什么 JDK 还要设计 Runnable 并采用代理模式呢?这种设计带来了巨大的架构优势:

  1. 彻底的解耦

    • 真实角色(Runnable:只关心业务逻辑(Task)。
    • 代理角色(Thread:只关心线程调度和生命周期管理(Worker)。
    • 如果业务逻辑改变了,你只需要修改 Runnable 的实现,不需要动任何线程控制的代码;反之,JVM 如果优化了线程调度(比如引入虚拟线程),你的业务代码也完全不需要改动。
  2. 绕过 Java 的单继承限制

    Java 类只能继承一个父类。如果你直接 extends Thread,这个类就不能再继承其他类了(比如无法继承 BaseService)。

    而采用代理模式,你的类只需要 implements Runnable,依然保留了继承其他类的权利。

  3. 方便资源共享

    代理模式使得同一个真实角色(Runnable)可以被多个代理角色(Thread)共同挂载。

    java
    MyTask task = new MyTask(); // 只有一份真实数据/逻辑
    
    // 多个代理同时执行这一个任务,天然适合资源共享(如买票系统、线程池池化技术)
    new Thread(task, "窗口 A").start();
    new Thread(task, "窗口 B").start();

总结

Java 通过 Runnable 接口创建线程的本质,就是通过 Thread 这个代理类,给你的 Runnable 真实任务套上了一层“能够被操作系统调度”的精美外壳。你负责编写纯粹的业务逻辑(真实角色),Thread 负责解决复杂的线程并发问题(代理角色),两者相得益彰。

练习:MiniThread

需求:实现一个 mini 的代理模式

java
package p01_create_thread.d02_create_thread;

public class D05E02MiniThread {
  public static void main(String[] args) {
    // 1. 方式一:
    MyTask task = new MyTask();
    new MiniThread(task).start();

    // 2. 方式二:
    new MiniThread(new Runnable() {
      @Override
      public void run() {
        System.out.println("匿名 MiniThread 正在运行...");
      }
    }).start();
  }
}

/**
 * Task 类
 */
class MyTask implements Runnable {
  @Override
  public void run() {
    System.out.println("MiniThread 正在运行...");
  }
}

/**
 * Thread 类
 */
class MiniThread implements Runnable {
  private Runnable target;

  public MiniThread(Runnable task) {
    this.target = task;
  }

  @Override
  public void run() {
    if(target != null) {
      target.run();
    }
  }

  public void start() {
    // 进行一系列检测
    start0();
  }

  private void start0() {
    run();
  };
}

Thread~

java.lang.Thread 是 Java 中进行多线程编程的最核心、最基础的类。在 Java 的世界里,任何代码的执行最终都是由 Thread 类的实例来驱动的。

理解 Thread 类的内部结构、构造方式以及它提供的丰富方法,是掌控 Java 并发编程的必经之路。

以下是对 Thread 类的全面解剖:

类的声明与本质

类的声明与本质:

打开 JDK 源码,你可以看到 Thread 类的声明如下:

java
public class Thread implements Runnable {
  // ...
}

这说明了一个非常重要的设计:Thread 类本身也实现了 Runnable 接口

这意味着,Thread 不仅是线程的“驱动器”(负责与底层操作系统交互并分配 CPU 资源),它自己也可以被看作是一个包含了 run() 方法的“任务”。当我们直接继承 Thread 类并重写 run() 方法时,其实就是把“驱动器”和“任务”绑定在了一起。

API:Thread

属性

每个 Thread 对象在底层都有几个极其关键的属性来标识它的状态和特征:

属性:

  • String name线程名称。每个线程都有一个名字。如果没有手动指定,Java 会自动生成类似 Thread-0, Thread-1 的名字。
    在实际开发中,强烈建议给线程起一个有业务意义的名字,这在排查日志和死锁时是救命的。

  • int priority线程优先级。范围从 1 (MIN_PRIORITY) 到 10 (MAX_PRIORITY),默认值是 5 (NORM_PRIORITY)。
    注意:优先级高的线程理论上获取 CPU 时间片的概率更大,但这完全取决于操作系统的具体实现。在 Java 开发中,绝对不能依赖线程优先级来控制业务逻辑的先后顺序。

  • boolean daemon是否为守护线程。分为用户线程(User Thread)守护线程(Daemon Thread)

  • Runnable target目标任务。这就是你在调用 new Thread(Runnable task) 时传进去的那个任务对象。

构造方法

Thread 类提供了多个重载的构造方法,最常用的有以下几个:

构造方法:

  • Thread()(),创建一个新的线程对象,名称自动生成

  • Thread()(String name),创建具有指定名称的线程对象。

  • Thread()(Runnable target),将一个 Runnable 任务对象传递给线程,由线程负责执行该任务。

  • Thread()(Runnable target, String name)最推荐的用法:同时指定要执行的任务和线程名称。

静态方法

为了便于记忆,我们可以将 Thread 类的方法分为三类:静态工具方法线程控制方法属性获取/设置方法

静态方法作用于当前正在执行的线程

  • Thread Thread.currentThread()()极度常用。返回对当前正在执行的线程对象的引用。常用于获取当前线程的名称:Thread.currentThread().getName()

  • void Thread.sleep()(long millis),让当前线程休眠(暂停执行)指定的毫秒数。进入 TIMED_WAITING 状态。注意:休眠期间不会释放已经持有的任何对象锁

  • void Thread.yield()()线程礼让当前线程主动提示调度器自己愿意让出 CPU 的使用权,状态由运行中(Running)变为就绪(Ready)。但操作系统可以选择忽略这个提示

实例方法:线程控制

实例方法:线程控制

  • void start()启动线程。通知 Java 虚拟机为其分配系统资源,并在就绪后调用该线程的 run() 方法。一个线程的 start() 方法只能被调用一次,否则抛 IllegalThreadStateException

  • void run()线程要执行的具体的业务代码实体。如果直接调用它,它会被当作当前线程下的一个普通方法执行,不会启动新线程

  • void join()线程插队。比如在 主线程 中调用 t1.join(),那么主线程会一直阻塞,直到 t1 线程执行完毕。常用于等待其他线程的计算结果。

  • void interrupt()中断线程。它并不会粗暴地立即停止线程,而是给目标线程打上一个“中断标记”。目标线程需要配合检查这个标记来决定是否安全地退出。

  • boolean isInterrupted()检查中断标记。测试该线程是否已经被中断。

  • boolean isAlive()测试该线程是否处于活动状态(已经 start() 且尚未终止)。

练习:线程插队【

main 线程创建一个子线程,每隔1s输出hello, 输出20次, 主线程每隔1秒, 输出hi, 输出20次.要求: 两个线程同时执行, 当主线程输出5次后, 就让子线程运行完毕, 主线程再继续。

java
public class D01_Thread_Join {
  public static void main(String[] args) throws InterruptedException {
    // 1. 创建子线程,每隔1s输出hello, 输出20次
    Thread subThread = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < 20; i++) {
          System.out.println(Thread.currentThread().getName() +  " Say: Hello" + (i+1));

          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }, "subThread");
    subThread.start();

    // 2. 主线程每隔1s输出hi, 输出20次
    for (int i = 0; i < 20; i++) {
      if(i == 5) {
        // 3. 线程插队
        subThread.join();
      }

      System.out.println(Thread.currentThread().getName() +  " Say: hi" + (i+1));


      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

实例方法:属性控制

实例方法:属性控制

  • final String getName()

    final void setName()(String name),获取/设置线程名

  • final int getPriority()

    final void setPriority()(int newPriority),获取/设置优先级

  • final boolean isDaemon()

    final void setDaemon()(boolean on),将该线程标记为守护线程或用户线程必须在 start() 方法之前调用,否则抛出异常。

注意事项

start() vs run()

误区一:start()run() 的天壤之别:

  • 调用 start():真正向操作系统申请创建了一个新的本地线程,新线程启动后会自动去执行 run() 里的代码。这叫多线程
  • 调用 run():仅仅是在当前线程中执行了一个名为 run普通对象方法而已,根本没有创建新线程。这叫同步执行

练习:sleep()

题目:如下的代码中 sleep() 执行后,哪个线程进入了阻塞状态?

image-20260506225542974

回答主线程进入了阻塞状态。

守护线程

在 Java 的多线程世界里,线程被分为两大阵营:用户线程(User Thread)守护线程(Daemon Thread)。如果把用户线程比作在台前表演的“主角”,那么守护线程就是默默在幕后打杂的“后勤人员”。

什么是守护线程

守护线程(Daemon Thread,后台线程) 唯一目的就是为其他非守护线程(用户线程)提供便利服务

  • JVM 的退出的生死判据:一个 Java 程序的生命周期与用户线程紧密相连。只要还有任何一个用户线程在运行,JVM 就不会退出(比如你的 main 线程就是一个典型的用户线程)。

  • 自动寿终正寝:当程序中的所有用户线程都执行完毕退出后,即使守护线程里的代码还在疯狂循环,JVM 也会毫不留情地直接关闭,所有的守护线程都会被立即强行终止

经典比喻:守护线程就像是游戏里的“召唤物”或操作系统的“后台常驻进程”。当英雄(用户线程)阵亡或通关(全部退出)时,召唤物(守护线程)不管手头活干没干完,都会瞬间凭空消失。

应用场景

在 Java 底层和日常开发中,守护线程经常被默默使用:

  • JVM 垃圾回收机制(GC):这是最著名的守护线程。它在后台默默监控内存,一旦发现垃圾就回收。当你的业务程序(用户线程)都跑完了,GC 也就没有存在的意义了,随 JVM 一同退出。

  • 后台监控与资源清理:例如,在应用启动时开启一个线程,每隔 5 分钟自动清理一次本地临时文件或内存缓存。这种定时任务极其适合设置为守护线程,因为它们不应该阻止主程序的正常关闭。

  • 心跳检测包发送:在 RPC 框架(如 Netty、Dubbo)或分布式系统中,用于维持长连接、定期向服务器发送心跳包的线程,通常也是守护线程。

创建守护线程

在 Java 中,任何一个通过 new Thread() 创建的线程,默认都是用户线程。要将其变成守护线程,需要在 start() 方法调用前通过 setDaemon(boolean on) 方法进行显式设置。

java
public class DaemonThreadDemo {
  public static void main(String[] args) throws InterruptedException {
    Thread daemonTask = new Thread(() -> {
      while (true) {
        try {
          System.out.println("守护线程:我在后台默默为你服务...");
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    // 【核心】将该线程标记为守护线程
    daemonTask.setDaemon(true); // 注意:必须在线程调用 start() 方法之前设置
    daemonTask.start(); // 启动线程

    // 主线程(用户线程)只运行 2 秒钟
    System.out.println("主线程:我要开始执行核心业务了...");
    Thread.sleep(2000);
    System.out.println("主线程:我的业务执行完毕,准备退出!");

    // 当主线程退出后,daemonTask 也会被 JVM 强行终止,控制台不会再打印
  }
}

守护线程 vs 用户线程

特性用户线程 (User Thread)守护线程 (Daemon Thread)
默认状态手动创建的线程默认为用户线程必须显式调用 setDaemon(true) 进行转换
职责定位负责执行核心业务逻辑辅助用户线程,提供后台支持(如 GC、清理)
对 JVM 寿命的影响
决定 JVM 的生死。只要有一个未结束,JVM 就不会停止
无法阻止 JVM 退出。用户线程死光,它立刻陪葬
生命周期由自身的业务逻辑(run 方法)决定何时结束随时可能在任意一行代码执行时被 JVM 强行掐断

注意事项

如果你决定在项目中使用守护线程,必须时刻警惕以下两个大坑:

  1. setDaemon() 的调用时机不对

    铁律:setDaemon(true) 必须在线程调用 start() 方法之前设置!

    如果你先写了 thread.start(),然后才写 thread.setDaemon(true),程序不仅不会变成守护线程,还会直接无情地抛出 IllegalThreadStateException 异常。

  2. finally 块中的代码可能永远不会执行

    这是初学者最容易踩的编译期和运行期认知不一致的坑。在 Java 的常规认知中,finally 块里的代码是“铁律执行”的。但是在守护线程中,这一铁律被彻底打破!

    java
    Thread daemonThread = new Thread(() -> {
      try {
        System.out.println("守护线程启动,开始写数据...");
        Thread.sleep(5000); // 模拟耗时的文件写入操作
      } finally {
        // 如果在 sleep 期间,所有的用户线程都退出了
        // JVM 会直接关闭,下面这行代码压根没有执行的机会!
        System.out.println("【危险】释放文件句柄、关闭数据库连接...");
      }
    });
    daemonThread.setDaemon(true);
    daemonThread.start();
  3. 避坑药方:绝对不要在守护线程中去执行任何需要严谨释放资源的操作

    释放资源的操作有关闭输入输出流、释放数据库连接、完成重大的文件落盘写入等。因为你无法预测它什么时候会突然死亡。这类需要安全收尾的任务,请老老实实交给用户线程去处理。

线程生命周期

要真正写出健壮的并发程序,或者在生产环境中排查 CPU 飙高、程序卡死(死锁)等问题,深刻理解 Java 线程的生命周期是必不可少的基本功。

API:Thread.State 枚举

在 Java 中,线程的生命周期并不是由操作系统直接决定的,而是由 JVM 明确规定在 java.lang.Thread.State 枚举类中的 6 种状态

java
public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

线程的生命周期状态

  • NEW新建。线程对象已经被创建出来,但是还没有调用 start() 方法

    • 状态解析: 此时它只是堆内存中的一个普通的 Java 对象,操作系统内核中还没有对应的底层线程。
    • 代码场景Thread t = new Thread();
  • RUNNABLE可运行。线程已经调用了 start() 方法,随时准备好执行,或者正在执行中。

    • 状态解析: 注意,这是一个极易误解的点! 在操作系统的层面,线程分为“就绪(Ready,等待 CPU 分配时间片)”和“运行(Running,正在 CPU 上执行)”。但在 Java 的世界里,JVM 将这两种状态合并统称为 RUNNABLE。因此,处于 RUNNABLE 状态的线程可能正在疯狂运行,也可能在排队等 CPU。
    • 状态流转: NEW -> 调用 t.start() -> RUNNABLE
  • BLOCKED阻塞。线程试图获取一个内部的对象锁(Monitor Lock),但该锁正被其他线程持有,因此当前线程被阻塞

    • 状态解析: 在 Java 中,只有在等待进入 synchronized 代码块或方法时,线程才会进入 BLOCKED 状态。等待 java.util.concurrent.locks.Lock(如 ReentrantLock)时,线程进入的是 WAITING 状态,而不是 BLOCKED
    • 状态流转:
      • RUNNABLE -> 竞争 synchronized 锁失败 -> BLOCKED
      • BLOCKED -> 成功获取到锁 -> RUNNABLE
  • WAITING等待。线程进入一种“无限期等待”的状态。它放弃了 CPU 的使用权,并且不会自动醒来,必须等待另一个线程执行特定的唤醒操作。

    • 状态解析: 处于这种状态的线程通常是在等待某个条件成立。
    • 触发条件(从 RUNNABLE 变为 WAITING):
      • 调用了没有设置超时时间的 Object.wait()
      • 调用了没有设置超时时间的 Thread.join()(本质上也是调用的 wait())。
      • 调用了 LockSupport.park()(JUC 锁的底层基石)。
    • 唤醒条件(从 WAITING 变为 RUNNABLE):
      • 其他线程调用了该对象的 Object.notify()Object.notifyAll()
      • LockSupport.unpark(Thread) 被调用。
  • TIMED_WAITING计时等待。类似于 WAITING,但它是“限时等待”。线程等待一段时间,如果时间到了还没有被唤醒,它会自动醒来并尝试继续执行。

    • 触发条件(从 RUNNABLE 变为 TIMED_WAITING):
      • Thread.sleep(long millis)(最常见,抱着锁睡觉)。
      • 带有超时参数的 Object.wait(long timeout)
      • 带有超时参数的 Thread.join(long millis)
      • LockSupport.parkNanos()LockSupport.parkUntil()
    • 唤醒条件(从 TIMED_WAITING 变为 RUNNABLE):
      • 等待时间到达。
      • 提前被 notify()notifyAll() 唤醒。
  • TERMINATED终止。线程的生命周期走到了尽头,已经停止运行

    • 状态解析: 一旦线程进入 TERMINATED 状态,就绝对不可能再复活。如果你尝试对一个已终止的线程再次调用 start() 方法,会抛出 IllegalThreadStateException 异常。
    • 触发条件:
      • run() 方法中的代码正常执行完毕。
      • 线程在执行过程中抛出了一个未捕获的异常(Exception 或 Error),导致线程意外死亡。

示例:线程状态演示

java
package p01_create_thread.d04_lifecycle;

public class D01_ThreadState {
  public static void main(String[] args) throws InterruptedException {
    MyThread myThread = new MyThread();

    // 1. 查看线程状态
    System.out.println("1. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // NEW
    myThread.start();

    while(Thread.State.TERMINATED != myThread.getState()) {
      // 3. 查看线程状态
      System.out.println("3. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // RUNNABLE、TIMED_WAITING
      Thread.sleep(500);
    }

    // 4. 查看线程状态
    System.out.println("4. " + myThread.getName() + " 线程此时的状态为:" + myThread.getState()); // TERMINATED
  }
}

class MyThread extends Thread {
  private boolean flag = true;
  @Override
  public void run() {
    while (flag) {
      // 2. 查看线程状态
      System.out.println("2. " + Thread.currentThread().getName() + " 线程正在运行,此时的状态为:" + Thread.currentThread().getState()); // RUNNABLE

      try {
        Thread.sleep(10000);
        flag = false;
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

  }
}

生命周期图解

线程的生命周期

JDK5及之后

image-20260506165726394

JDK5之前

image-20260506165145384

易错点总结

核心面试/实战易错点总结:

为了帮你更好地消化,这里对比几个容易混淆的场景:

场景区别核心说明
BLOCKED vs WAITINGBLOCKED 是因为“别人抢了我的,我只能干等”;WAITING 是因为“我自己主动退居幕后,等待别人给我发信号”。
sleep() vs wait()sleep() 进入 TIMED_WAITING,且不释放锁wait() 进入 WAITING,且必须释放锁,让出临界区资源。
JUC 锁 vs synchronized使用 ReentrantLock.lock() 等不到锁时,线程进入的是 WAITING 状态(底层是 LockSupport.park()),而不是 BLOCKED

理解了线程的生命周期后,我们在实际开发中往往需要对这些状态进行干预,比如在一个线程处于 WAITINGTIMED_WAITING 时安全地打断(interrupt())它。

练习

练习1:新年倒计时

模拟新年倒计时,每隔1秒输出一个数字,依次输出:10,9,8...1,最后输出:新年快乐!

代码实现

java
public class D02E01_HappyNewYear {
  public static void main(String[] args) throws InterruptedException {
    System.out.println("进入倒计时:");
    for (int i = 10; i > 0; i--) {
      System.out.println(i);

      Thread.sleep(1000);
    }

    System.out.println("新年快乐!");
  }
}

练习2:

在main方法中启动两个线程:第1个线程循环随机打印100以内的整数,直到第2个线程从键盘读取了 “Q” 命令。

java
package p01_create_thread.d04_lifecycle;

import java.util.Scanner;

/**
 * 在main方法中启动两个线程:第1个线程循环随机打印100以内的整数,直到第2个线程从键盘读取了 “Q” 命令。
 */
public class D03E02_InterruptThread {
  public static void main(String[] args) {
    // 1. 创建线程 t1
    T1 t1 = new T1();
    t1.setName("T1");
    t1.start();


    // 2. 创建线程 t2
    T2 t2 = new T2(t1);
    t2.setName("T2");
    t2.start();
  }
}

// 第一个线程
class T1 extends Thread {
  private boolean flag = true;
  @Override
  public void run() {
    while(flag) {
      System.out.println((int) (Math.random() * 101));

      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public void setFlag(boolean flag) {
    this.flag = flag;
  }
}

// 第二个线程
class T2 extends Thread {
  private T1 t1 = null;

  public T2(T1 t1) {
    this.t1 = t1;
  }

  @Override
  public void run() {
    // Scanner
    Scanner scanner = new Scanner(System.in);

    while (true) {
      System.out.println("请输入终止键(Q)结束 T1 线程:");
      char c = scanner.next().toUpperCase().charAt(0);

      if((char)'Q' == c) {
        t1.setFlag(false);
        break;
      }
    }

    scanner.close();
  }
}